/*******************************************************************************
 * Copyright (c) 2005 Koji Hisano <hisano@users.sourceforge.net>
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 *
 * Contributors:
 *     Koji Hisano - initial API and implementation
 *******************************************************************************/
package jp.sf.mapswidgets;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.ProgressAdapter;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.StatusTextEvent;
import org.eclipse.swt.browser.StatusTextListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Widget;

/**
 * Instances of this class implement the Google Maps browser.
 * <p>
 * Note that although this class is a subclass of <code>Composite</code>,
 * it does not make sense to set a layout on it.
 * </p><p>
 * IMPORTANT: This class is <em>not</em> intended to be subclassed.
 * </p>
 */
public class GoogleMaps extends Composite {
	
	private boolean loadCompleted;
	private final List<String> commandsBeforeLoading = new ArrayList<String>();
	private final ProgressAdapter completedListener;
	private Runnable runAfterLoading;

	private final List<Overlay> overlays = new ArrayList<Overlay>();

	private final Browser browser;

	private final List<GoogleMapsListener> listeners = new ArrayList<GoogleMapsListener>();
	private StatusTextListener listener;
	
	private boolean debugEnabled;
	
	/**
	 * Constructs a new instance of this class given its parent
	 * and a style value describing its behavior and appearance.
	 * <p>
	 * The style value is either one of the style constants defined in
	 * class <code>SWT</code> which is applicable to instances of this
	 * class, or must be built by <em>bitwise OR</em>'ing together 
	 * (that is, using the <code>int</code> "|" operator) two or more
	 * of those <code>SWT</code> style constants. The class description
	 * lists the style constants that are applicable to the class.
	 * Style bits are also inherited from superclasses.
	 * </p>
	 *
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 *
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
	 * </ul>
	 * @exception SWTException <ul>
	 *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
	 * </ul>
	 * @exception SWTError <ul>
	 *    <li>ERROR_NO_HANDLES if a handle could not be obtained for browser creation</li>
	 * </ul>
	 * 
	 * @see Widget#getStyle
	 */
	public GoogleMaps(Composite parent, int style) {
		this(parent, style, "http://mapswidgets.sourceforge.jp/html/ie.html");
	}

	/**
	 * Constructs a new instance of this class given its parent
	 * and a style value describing its behavior and appearance.
	 * <p>
	 * The style value is either one of the style constants defined in
	 * class <code>SWT</code> which is applicable to instances of this
	 * class, or must be built by <em>bitwise OR</em>'ing together 
	 * (that is, using the <code>int</code> "|" operator) two or more
	 * of those <code>SWT</code> style constants. The class description
	 * lists the style constants that are applicable to the class.
	 * Style bits are also inherited from superclasses.
	 * </p>
	 *
	 * @param parent a widget which will be the parent of the new instance (cannot be null)
	 * @param style the style of widget to construct
	 * @param url the url of a Google Maps included page
	 *
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
	 * </ul>
	 * @exception SWTException <ul>
	 *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
	 * </ul>
	 * @exception SWTError <ul>
	 *    <li>ERROR_NO_HANDLES if a handle could not be obtained for browser creation</li>
	 * </ul>
	 * 
	 * @see Widget#getStyle
	 */
	public GoogleMaps(Composite parent, int style, String url) {
		super(parent, checkStyle(style));

		execute("map.elements = new Array();");

		setLayout(new FillLayout());

		browser = new Browser(this, SWT.NONE);
		browser.setUrl(url);

		completedListener = new ProgressAdapter() {
			@Override
			public void completed(ProgressEvent event) {
				onCompleted(event);
				browser.removeProgressListener(completedListener);
			}
		};
		browser.addProgressListener(completedListener);
	}
	
	private static int checkStyle(int style) {
		return SWT.NONE;
	}

	void onCompleted(ProgressEvent event) {
		loadCompleted = true;
		for (String script : commandsBeforeLoading) {
			browser.execute(script);
		}
		commandsBeforeLoading.clear();
		initListener();
		if (runAfterLoading != null) {
			runAfterLoading.run();
		}
	}
	
	/**
	 * Set the code running after this component is initialized (Google Maps page loading finished).
	 * 
	 * @param runAfterLoading the code after initialized
	 * 
	 * @exception SWTException if the code is already setted 
	 */
	public void runAfterLoading(Runnable runAfterLoading) {
		if (this.runAfterLoading != null) {
			throw new SWTException("runAfterLoading is already setted");
		}
		this.runAfterLoading = runAfterLoading;
	}

	/**
	 * Returns the inner browser component.
	 *
	 * @return the inner browser component
	 */
	Browser getBrowser() {
		return browser;
	}
	
	/**
	 * Turns on/off the debug mode.
	 * 
	 * @param on <ul>
	 *   <li>true - turns on</li>
	 *   <li>false - turns off</li>
	 * </ul>
	 */
	public void setDebug(boolean on) {
		this.debugEnabled = on;
	}

	/**
	 * Executes a java script code
	 * 
	 * @param script a java script code
	 * 
	 * @return the execution result <ul>
	 *   <li>true - success</li>
	 *   <li>false - failure</li>
	 * </ul>
	 */
	public boolean execute(final String script) {
		if (debugEnabled) {
			System.out.println(script);
		}
		if (loadCompleted) {
			final boolean[] result = new boolean[1];
			getDisplay().syncExec(new Runnable() {
				public void run() {
					result[0] = browser.execute(script);
				}
			});
			return result[0];
		} else {
			commandsBeforeLoading.add(script);
			return true;
		}
	}

	/**
	 * Executes a java script code and get a result.
	 * <p>
	 * If you want to get a result value from a java script code, you must set the status bar by code.
	 * </p>
	 * 
	 * @param script a java script code
	 * 
	 * @return the current status bar text
	 */
	public synchronized String executeWithResult(String script) {
		if (!loadCompleted) {
			throw new SWTException("Loading a widget page is not completed (use runAfterLoading method to resolve this)");
		}
		final String[] result = new String[1];
		StatusTextListener listener = new StatusTextListener() {
			public void changed(StatusTextEvent event) {
				result[0] = event.text;
				GoogleMaps.this.notify();
			}
		};
		browser.addStatusTextListener(listener);
		execute(script);
		try {
			wait(500);
		} catch (InterruptedException e) {
		} finally {
			browser.removeStatusTextListener(listener);
		}
		return result[0];
	}

	/**
	 * Enables/Disables dynamic dragging (enabled by default).
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; enableDragging()/disableDragging()</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - enabled</li>
	 *   <li>false - disabled</li>
	 * </ul>
	 */
	public void setDraggingEnabled(boolean on) {
		if (on) {
			execute("map.enableDragging();");
		} else {
			execute("map.disableDragging();");
		}
	}

	/**
	 * Returns true if dynamic dragging is enabled.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; draggingEnabled()</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - enabled</li>
	 *   <li>false - disabled</li>
	 * </ul>
	 */
	public boolean isDraggingEnabled() {
		return getBooleanProperty("map.draggingEnabled()");
	}

	/**
	 * Enables/Disables the info window on this map (enabled by default).
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; enableInfoWindow()/disableInfoWindow()</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - enabled</li>
	 *   <li>false - disabled</li>
	 * </ul>
	 */
	public void setInfoWindowEnabled(boolean on) {
		if (on) {
			execute("map.enableInfoWindow();");
		} else {
			execute("map.disableInfoWindow();");
		}
	}

	/**
	 * Returns true if the info window is enabled on this map.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; infoWindowEnabled()</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - enabled</li>
	 *   <li>false - disabled</li>
	 * </ul>
	 */
	public boolean isInfoWindowEnabled() {
		return getBooleanProperty("map.infoWindowEnabled()");
	}

	/**
	 * Shows/Hieds the large pan/zoom map control.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GLargeMapControl</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public void setLargeMapControlVisible(boolean on) {
		if (on) {
			execute("map.largeMapControl = new GLargeMapControl(); map.addControl(map.largeMapControl);");
		} else {
			execute("if(map.largeMapControl) {map.removeControl(map.largeMapControl); delete map.largeMapControl;}");
		}
	}

	/**
	 * Returns true if the large pan/zoom map control is showed.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GLargeMapControl</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public boolean isLargeMapControlVisible() {
		return getBooleanProperty("map.largeMapControl");
	}

	/**
	 * Shows/Hieds the small pan/zoom map control.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GSmallMapControl</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public void setSmallMapControlVisible(boolean on) {
		if (on) {
			execute("map.smallMapControl = new GSmallMapControl(); map.addControl(map.smallMapControl);");
		} else {
			execute("if(map.smallMapControl) {map.removeControl(map.smallMapControl); delete map.smallMapControl;}");
		}
	}

	/**
	 * Returns true if the small pan/zoom map control is showed.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GSmallMapControl</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public boolean isSmallMapControlVisible() {
		return getBooleanProperty("map.smallMapControl");
	}

	/**
	 * Shows/Hieds the small zoom map control (no panning control).
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GSmallZoomControl</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public void setSmallZoomControlVisible(boolean on) {
		if (on) {
			execute("map.smallZoomControl = new GSmallZoomControl(); map.addControl(map.smallZoomControl);");
		} else {
			execute("if(map.smallZoomControl) {map.removeControl(map.smallZoomControl); delete map.smallZoomControl;}");
		}
	}

	/**
	 * Returns true if the small zoom map control (no panning control) is showed.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GSmallZoomControl</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public boolean isSmallZoomControlVisible() {
		return getBooleanProperty("map.smallZoomControl");
	}

	/**
	 * Shows/Hieds the map type control.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GMapTypeControl</a>].
	 * </p>
	 * 
	 * @param on <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public void setMapTypeControlVisible(boolean on) {
		if (on) {
			execute("map.mapTypeControl = new GMapTypeControl(); map.addControl(map.mapTypeControl);");
		} else {
			execute("if(map.mapTypeControl) {map.removeControl(map.mapTypeControl); delete map.mapTypeControl;}");
		}
	}

	/**
	 * Returns true if the map type control is showed.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#Controls_overview">API Overview &gt; Controls &gt; GMapTypeControl</a>].
	 * </p>
	 *
	 * @return <ul>
	 *   <li>true - showed</li>
	 *   <li>false - hidden</li>
	 * </ul>
	 */
	public boolean isMapTypeControlVisible() {
		return getBooleanProperty("map.mapTypeControl");
	}

	private boolean getBooleanProperty(String expression) {
		return Boolean.parseBoolean(executeWithResult("if(" + expression + ") {window.status = 'true';} else {window.status = 'false';}"));
	}

	/**
	 * Returns the center point of the map viewport in latitude/longitude coordinates.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; getCenterLatLng()</a>].
	 * </p>
	 * 
	 * @return the center point of the map viewport
	 */
	public Point getCenter() {
		String result = executeWithResult("window.status = map.getCenterLatLng().x + ',' + map.getCenterLatLng().y;");
		String[] points = result.split(",");
		return new Point(Double.parseDouble(points[0]), Double.parseDouble(points[1]));
	}

	/**
	 * Returns the latitude/longitude bounds of the map viewport.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; getBoundsLatLng()</a>].
	 * </p>
	 * 
	 * @return the bounds of the map viewport
	 */
	public Bounds getMapBounds() {
		String result = executeWithResult("window.status = map.getBoundsLatLng().minX + ',' + map.getBoundsLatLng().minY + ',' + map.getBoundsLatLng().maxX + ',' + map.getBoundsLatLng().maxY;");
		String[] points = result.split(",");
		return new Bounds(Double.parseDouble(points[0]), Double.parseDouble(points[1]), Double.parseDouble(points[2]), Double.parseDouble(points[3]));
	}

	/**
	 * Returns the width and height of the map viewport in latitude/longitude ticks.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; getSpanLatLng()</a>].
	 * </p>
	 * 
	 * @return the width and height of the map viewport
	 */
	public Size getMapSpan() {
		String result = executeWithResult("window.status = map.getSpanLatLng().width + ',' + map.getSpanLatLng().height");
		String[] points = result.split(",");
		return new Size(Double.parseDouble(points[0]), Double.parseDouble(points[1]));
	}

	/**
	 * Returns the integer zoom level of the map.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; getZoomLevel()</a>].
	 * </p>
	 * 
	 * @return the zoom level of the map
	 */
	public int getZoomLevel() {
		return Integer.parseInt(executeWithResult("window.status = map.getZoomLevel();"));
	}

	/**
	 * Centers the map at the given point.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; centerAtLatLng(latLng)</a>].
	 * </p>
	 * 
	 * @param newPoint a new center point
	 */
	public void setCenter(Point newPoint) {
		execute("map.setCenter(" + newPoint.getExpression() + ");");
	}

	/**
	 * Centers the map at the given point, doing a fluid pan to the point if it is within the current map viewport.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; recenterOrPanToLatLng(latLng)</a>].
	 * </p>
	 * 
	 * @param targetPoint a new target point
	 */
	public void moveTo(Point targetPoint) {
		execute("map.recenterOrPanToLatLng(" + targetPoint.getExpression() + ");");
	}

	/**
	 * Zooms to the given integer zoom level, ignoring the request if the given zoom level is outside the bounds of the current map type.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; zoomTo(zoomLevel)</a>].
	 * </p>
	 * 
	 * @param newLevel a new zoom level
	 */
	public void setZoomLevel(int newLevel) {
		execute("map.zoomTo(" + newLevel + ");");
	}

	/**
	 * Returns an array of map types supported by this map.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; getMapTypes()</a>].
	 * </p>
	 * 
	 * @return the supported map types
	 */
	public MapType[] getSupportedMapTypes() {
		String result = executeWithResult("" + "window.status = '';\n" + "for (var i in map.getMapTypes()) {\n" + "  switch(map.getMapTypes()[i]) {\n" + "    case G_MAP_TYPE:\n" + "      window.status += '[MAP]';\n" + "      break;\n"
				+ "    case G_HYBRID_TYPE:\n" + "      window.status += '[HYBRID]';\n" + "      break;\n" + "    case G_SATELLITE_TYPE:\n" + "      window.status += '[SATELLITE]';\n" + "      break;\n" + "  }\n" + "}\n");
		List<MapType> mapTypes = new ArrayList<MapType>();
		for (MapType type : MapType.values()) {
			if (result.contains(type.toString())) {
				mapTypes.add(type);
			}
		}
		return mapTypes.toArray(new MapType[0]);
	}

	/**
	 * Returns the map type currently in use.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; getCurrentMapType()]</a>.
	 * </p>
	 * 
	 * @return the current map type
	 */
	public MapType getMapType() {
		String result = executeWithResult("" + "switch(map.getCurrentMapType()) {\n" + "  case G_MAP_TYPE:\n" + "    window.status = 'MAP';\n" + "    break;\n" + "  case G_HYBRID_TYPE:\n" + "    window.status = 'HYBRID';\n" + "    break;\n"
				+ "  case G_SATELLITE_TYPE:\n" + "    window.status = 'SATELLITE';\n" + "    break;\n" + "}\n");
		for (MapType type : MapType.values()) {
			if (result.equals(type.toString())) {
				return type;
			}
		}
		throw new IllegalStateException("not implemented map type");
	}

	/**
	 * Switches this map to the given map type.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; setMapType(mapType)]</a>.
	 * </p>
	 * 
	 * @param newMapType a new map type
	 */
	public void setMapType(MapType newMapType) {
		execute("map.setMapType(G_" + newMapType + "_TYPE);");
	}

	/**
	 * Adds the given overlay object (Marker or Polyline) to the map.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; addOverlay(overlay)]</a>.
	 * </p>
	 * 
	 * @param added an added overlay
	 */
	public void addOverlay(Overlay added) {
		added.init(this);
		execute("map.addOverlay(" + added.getVariable() + ");");
	}

	/**
	 * Removes the given overlay object from the map.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; removeOverlay(overlay)]</a>.
	 * </p>
	 * 
	 * @param removed the removed overlay
	 */
	public void removeOverlay(Overlay removed) {
		execute("map.removeOverlay(" + removed.getVariable() + ");");
		removed.destroy(this);
	}

	/**
	 * Removes all of the overlays from the map.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; clearOverlays()]</a>.
	 * </p>
	 */
	public void removeAllOverlays() {
		execute("map.clearOverlays();");
		for (Overlay overlay: overlays) {
			overlay.destroy(this);
		}
	}
	
	List<Overlay> getOverlaysList() {
		return overlays;
	}
	
	/**
	 * Displays the info window with the given HTML content at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; openInfoWindowHtml(latLng, htmlStr)]</a>.
	 * </p>
	 * 
	 * @param point the point of the info window
	 * @param html the content of the info window
	 */
	public void showInfoWindow(Point point, String html) {
		CheckUtils.isNotNull("point", point);
		CheckUtils.isNotNull("html", html);
		execute("map.openInfoWindowHtml(" + point.getExpression() + ", '" + html + "');");
	}
	
	/**
	 * Displays the info window with the given HTML content at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; openInfoWindowHtml(latLng, htmlStr, pixelOffset)]</a>.
	 * </p>
	 * 
	 * @param point the point of the info window
	 * @param html the content of the info window
	 * @param offset the offset of the info window
	 */
	public void showInfoWindow(Point point, String html, Size offset) {
		CheckUtils.isNotNull("point", point);
		CheckUtils.isNotNull("html", html);
		CheckUtils.isNotNull("offset", offset);
		execute("map.openInfoWindowHtml(" + point.getExpression() + ", '" + html + "', " + offset.getExpression() + ");");
	}
	
	/**
	 * Shows a blowup of the map at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; showMapBlowup(point)]</a>.
	 * </p>
	 * 
	 * @param point the point of the map window
	 */
	public void showMapWindow(Point point) {
		CheckUtils.isNotNull("point", point);
		execute("map.showMapBlowup(" + point.getExpression() + ");");
	}
	
	/**
	 * Shows a blowup of the map at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; showMapBlowup(point, zoomLevel)]</a>.
	 * </p>
	 * 
	 * @param point the point of the map window
	 * @param zoomLevel the zoom level in the map window
	 */
	public void showMapWindow(Point point, int zoomLevel) {
		CheckUtils.isNotNull("point", point);
		execute("map.showMapBlowup(" + point.getExpression() + ", " + zoomLevel + ");");
	}
	
	/**
	 * Shows a blowup of the map at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; showMapBlowup(point, zoomLevel, mapType)]</a>.
	 * </p>
	 * 
	 * @param point the point of the map window
	 * @param zoomLevel the zoom level in the map window
	 * @param type the map type in the map window
	 */
	public void showMapWindow(Point point, int zoomLevel, MapType type) {
		CheckUtils.isNotNull("point", point);
		execute("map.showMapBlowup(" + point.getExpression() + ", " + zoomLevel + ", G_" + type + "_TYPE);");
	}
		
	/**
	 * Shows a blowup of the map at the given point.
	 * <p>
	 * See Google Maps API documentation <a href="http://www.google.com/apis/maps/documentation/#GMap_code_">[Class Reference &gt; GMap &gt; showMapBlowup(point, zoomLevel, mapType, pixelOffset)]</a>.
	 * </p>
	 * 
	 * @param point the point of the map window
	 * @param zoomLevel the zoom level in the map window
	 * @param type the map type in the map window
	 * @param offset the offset of the map window
	 */
	public void showMapWindow(Point point, int zoomLevel, MapType type, Size offset) {
		CheckUtils.isNotNull("point", point);
		CheckUtils.isNotNull("offset", offset);
		execute("map.showMapBlowup(" + point.getExpression() + ", " + zoomLevel + ", G_" + type + "_TYPE, " + offset.getExpression() + ");");
	}
	
	/**
	 * Closes the window if it is open.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; closeInfoWindow()</a>].
	 * </p>
	 */
	public void hideWindow() {
		execute("map.closeInfoWindow()");
	}

	/**
	 * Adds the listener to the collection of listeners who will
	 * be notified when the Google Maps status is changed, by sending
	 * it one of the messages defined in the <code>GoogleMapsListener</code>
	 * interface.
	 * <p>
	 * See Google Maps API documentation [<a href="http://www.google.com/apis/maps/documentation/#GMap_code_">Class Reference &gt; GMap &gt; Events</a>].
	 * </p>
	 * <p>
	 * <code>clicked(Overlay clicked)</code> is called when the user clicks the overlay on the map.
	 * <code>clicked(Point point)</code> is called when the user clicks the map.
	 * <code>moved()</code> is called when the map is moving. This event is triggered continuously as the map is dragged.
	 * <code>moveStarted()</code> is called at the beginning of a continuous pan/drag movement. This event is not triggered when the map moves discretely.
	 * <code>moveEnded()</code> is called at the end of a discrete or continuous map movement. This event is triggered once at the end of a continuous pan.
	 * <code>zoomed(int oldZoomLevel, int newZoomLevel)</code> is called after the map zoom level changes.
	 * <code>mapTypeChanged()</code> is called after the map type (Map, Hybrid, or Satellite) changes.
	 * <code>windowOpend()</code> is called after the info window is displayed.
	 * <code>windowClosed()</code> is called after the info window is closed.
	 * <code>overlayAdded(Overlay added)</code> is called after an overlay is added to the map.
	 * <code>overlayRemoved(Overlay removed)</code> is called after an overlay is removed from the map.
	 * <code>allOverlaysRemoved()</code> is called after all overlays are cleared from the map.
	 * </p>
	 *
	 * @param listener the listener which should be notified
	 *
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
	 * </ul>
	 *
	 * @see GoogleMapsListener
	 * @see GoogleMapsAdapter
	 * @see #removeGoogleMapsListener
	 */
	public void addGoogleMapsListener(GoogleMapsListener listener) {
		if (listener == null) {
			throw new SWTException(SWT.ERROR_NULL_ARGUMENT);
		}
		listeners.add(listener);
		initListener();
	}

	/**
	 * Removes the listener from the collection of listeners who will
	 * be notified when the Google Maps status is changed.
	 *
	 * @param listener the listener which should no longer be notified
	 *
	 * @exception IllegalArgumentException <ul>
	 *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
	 * </ul>
	 *
	 * @see GoogleMapsListener
	 * @see GoogleMapsAdapter
	 * @see #addGoogleMapsListener
	 */
	public void removeGoogleMapsListener(GoogleMapsListener listener) {
		if (listener == null) {
			throw new SWTException(SWT.ERROR_NULL_ARGUMENT);
		}
		listeners.remove(listener);
		destroyListener(false);
	}

	private void initListener() {
		if (!listeners.isEmpty() && listener == null) {
			listener = new StatusTextListener() {
				public void changed(StatusTextEvent event) {
					String header = "GoogleMaps:";
					if (event.text.startsWith(header)) {
						execute("window.status = '';");
						handleEvent(event.text.substring(header.length()));
					}
				}
			};
			getBrowser().addStatusTextListener(listener);
			execute(""
					+ "map.click = function(overlay, point) {\n"
					+ "  if (overlay) {\n"
					+ "    window.status = 'GoogleMaps:clickOverlay(' + overlay.id + ')';\n"
					+ "  }\n"
					+ "  if (point) {\n"
					+ "    window.status = 'GoogleMaps:clickPoint(' + point.x + ',' + point.y + ')';\n"
					+ "  }\n"
					+ "};\n"
					+ "GEvent.addListener(map, 'click', map.click);\n"
					+ "map.move = function() {window.status = 'GoogleMaps:move';}; GEvent.addListener(map, 'move', map.move);\n"
					+ "map.movestart = function() {window.status = 'GoogleMaps:movestart';}; GEvent.addListener(map, 'movestart', map.movestart);\n"
					+ "map.moveend = function() {window.status = 'GoogleMaps:moveend';}; GEvent.addListener(map, 'moveend', map.moveend);\n"
					+ "map.zoom = function(oldZoomLevel, newZoomLevel) {\n"
					+ "  window.status = 'GoogleMaps:zoom(' + oldZoomLevel + ',' + newZoomLevel + ')';\n"
					+ "};\n"
					+ "GEvent.addListener(map, 'zoom', map.zoom);\n"
					+ "map.maptypechanged = function() {window.status = 'GoogleMaps:maptypechanged';}; GEvent.addListener(map, 'maptypechanged', map.maptypechanged);\n"
					+ "map.infowindowopen = function() {window.status = 'GoogleMaps:infowindowopen';}; GEvent.addListener(map, 'infowindowopen', map.infowindowopen);\n"
					+ "map.infowindowclose = function() {window.status = 'GoogleMaps:infowindowclose';}; GEvent.addListener(map, 'infowindowclose', map.infowindowclose);\n"
					+ "map.addoverlay = function(overlay) {\n"
					+ "  window.status = 'GoogleMaps:addoverlay(' + overlay.id + ')';\n"
					+ "};\n"
					+ "GEvent.addListener(map, 'addoverlay', map.addoverlay);\n"
					+ "map.removeoverlay = function(overlay) {\n"
					+ "  window.status = 'GoogleMaps:removeoverlay(' + overlay.id + ')';\n"
					+ "};\n"
					+ "GEvent.addListener(map, 'removeoverlay', map.removeoverlay);\n"
					+ "map.clearoverlays = function() {window.status = 'GoogleMaps:clearoverlays';}; GEvent.addListener(map, 'clearoverlays', map.clearoverlays);");
		}
	}

	private void handleEvent(String event) {
		if (event.startsWith("clickOverlay")) {
			String[] arguments = parseArguments(event);
			for (GoogleMapsListener listener : listeners) {
				listener.clicked(findOverlay(arguments[0]));
			}
		} else if (event.startsWith("clickPoint")) {
			String[] arguments = parseArguments(event);
			for (GoogleMapsListener listener : listeners) {
				listener.clicked(new Point(Double.parseDouble(arguments[0]), Double.parseDouble(arguments[1])));
			}
		} else if ("move".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.moved();
			}
		} else if ("movestart".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.moveStarted();
			}
		} else if ("moveend".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.moveEnded();
			}
		} else if (event.startsWith("zoom")) {
			String[] arguments = parseArguments(event);
			for (GoogleMapsListener listener : listeners) {
				listener.zoomed(Integer.parseInt(arguments[0]), Integer.parseInt(arguments[1]));
			}
		} else if ("maptypechanged".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.mapTypeChanged();
			}
		} else if ("infowindowopen".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.windowOpend();
			}
		} else if ("infowindowclose".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.windowClosed();
			}
		} else if (event.startsWith("addoverlay")) {
			String[] arguments = parseArguments(event);
			for (GoogleMapsListener listener : listeners) {
				listener.overlayAdded(findOverlay(arguments[0]));
			}
		} else if (event.startsWith("removeoverlay")) {
			String[] arguments = parseArguments(event);
			for (GoogleMapsListener listener : listeners) {
				listener.overlayRemoved(findOverlay(arguments[0]));
			}
		} else if ("clearoverlays".equals(event)) {
			for (GoogleMapsListener listener : listeners) {
				listener.windowOpend();
			}
		}
	}

	private String[] parseArguments(String event) {
		int start = event.indexOf('(') + 1;
		int end = event.length() - 1;
		return event.substring(start, end).split(",");
	}

	private Overlay findOverlay(String id) {
		for (Overlay overlay : overlays) {
			if (overlay.getId().equals(id)) {
				return overlay;
			}
		}
		return null;
	}

	private void destroyListener(boolean force) {
		if ((force || listeners.isEmpty()) && listener != null) {
			getBrowser().removeStatusTextListener(listener);
			execute(""
					+ "GEvent.removeListener(map.click);\n" + "GEvent.removeListener(map.move);\n" + "GEvent.removeListener(map.movestart);\n" + "GEvent.removeListener(map.moveend);\n" + "GEvent.removeListener(map.zoom);\n"
					+ "GEvent.removeListener(map.maptypechanged);\n" + "GEvent.removeListener(map.infowindowopen);\n" + "GEvent.removeListener(map.infowindowclose);\n" + "GEvent.removeListener(map.addoverlay);\n"
					+ "GEvent.removeListener(map.removeoverlay);\n" + "GEvent.removeListener(map.clearoverlays);");
			listener = null;
		}
	}

	public void setAddress(String address) {
		CheckUtils.isNotNull("address", address);
		StringBuilder sb = new StringBuilder();
		sb.append("var geocoder = new GClientGeocoder();");
		sb.append("geocoder.getLatLng('").append(address.replaceAll("'", "\\\\\\'")).append("', function(point) {");
		sb.append("if (point) {");
		sb.append("map.setCenter(point, 13);");
		sb.append("var marker = new GMarker(point);");
		sb.append("map.addOverlay(marker);");
		sb.append("}");
		sb.append("});");		
		execute(sb.toString());
	}
	
}
